"""DataK items.

Defines the base class for all items and the Item, Link and Group classes.

# LICENSE ....................................................................
    This file is part of DataK.

    DataK is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    DataK is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with DataK.  If not, see <http://www.gnu.org/licenses/>.
# ............................................................................
"""

from singleton import ClassKeySingleton as _ClassKeySingleton

#
#--- _AbstractItem
#
class _AbstractItem( _ClassKeySingleton ):
    '''
    The AbstractItem knows its connection and its id.
    The instances are singletonized on the connection.id/id pairs.
    
    This class is an interface to a serie of strategies:
        Revision Control
        Attributes
        Search
        Sort
        Group
        
    It is used as context for those strategies.
    The strategies are provided by the connection in the constructor.
    Subclasses (Item, Link, Group) alter the singleton policy.
    
    '''
    def __init__( self, cnt, id ):
        '''
        Create a new Item.
        Request strategies from the connection.
        '''
        _ClassKeySingleton.__init__( self )
        self._cnt = cnt
        self._id = id.strip('/')
        
        self._cnt.setup_item( self )
        #self._rc = cnt.GetRCStrategy(self)
        #self._attrs = cnt.GetAttributeStrategy(self)
        #self._selecter = cnt.GetSelectStrategy(self)
        #self._sorter = SortStrategy()
        #self._grouper = GroupStrategy()
    
    def __repr__( self ):
        return _ClassKeySingleton.__repr__(self)[:-1]+' '+self.id+'>'
    
    def __str__( self ):
        '''
        str( item )
        
        Return the string representation of the item's value.
        
        '''
        return str( self.value )
    
    def __call__( self, expression=None ):
        '''
        item( expression )
        
        Evaluate the string expression and return the result.
        If expression is None, True is returned.
        
        An expression can use wilcarded ids and '@' to reference items:
            '@.attrName' <==> item.attrName
            '@SUB/*'     <==> item['SUB/*']
            '@/LIB'      <==> item['/LIB']
            '@[3:]       <==> item[3:]
            etc...
        and python code:
            '@.attrName * @SUB.attrName'
            '[ i.start_date for i in @TASK if i.user == "dee" ]'
        
        if the expression is made of a single word, it evaluates to the
        attribute named by this word:
            item('name') == item.name
            
        '''
        # TODO: this should be done by _selecter.Where or 
        # by a new stratehy _expressions.Eval (which should then
        # be used by _selecter.Where)
        if ' ' not in expression and '@' not in expression:
            return self.__getattr__( expression )
            
        return expression is None or self._selecter.Where( self, expression )
        
    def __getattr__( self, name ):
        '''
        item.attrName
        
        If attrName ends with a '_' its value will be herited from it parent.
        
        Unexisting attributes return None.
        Those attributes are always defined:
            id
            path
            name
            value

        '''
        # property like attributes:
        if name == 'id':
            return self._id
        elif name == 'path':
            return self._cnt.GetPathFromId( self._id )
        elif name == 'name':
            return os.path.basename(self._id)
        
        # python attributes:
        elif name in self.__dict__:
            return self.__getattribute__(name)
            
        # DataK attributes
        if name.endswith('_'):
            # TODO: shouldn't this be done by the strategy ?
            return self._attrs.Get( self, name[-1], herited=True )
        elif name == 'value':
            # the value attribute default is the item name
            # TODO: this should be done by the strategy
            return self._attrs.Get( self, 'value' ) or self.name
        return self._attrs.Get( self, name, herited=False )

    def __setattr__( self, name, value ):
        '''
        item.attrName = x
        
        Trying to set on those attributes is an error:
            id
            path
            name
        
        Setting to an unexisting attribute creates it.
        Setting an attribute to None deletes it.

        '''
        # python atrtibutes
        if name[0] == '_':
            return object.__setattr__( self, name, value )
        
        # read only property like attributes
        if name in ('id','path','name'):
            raise ValueError( name+' is Read Only!') 

        # DataK attributes
        if value is None:
            self._attrs.Del( self, name )
        else:
            self._attrs.Set( self, name, value )

    def __eq__( self, other ):
        '''
        itemX == itemY
        item == 18
        
        Two items are equal if they are the same item.
        An item is equal to something else if its value is.
        
        To test if two items have the same value, you can:
            itemX == itemY.value
        but this will never be True:
            itemX.value == itemY
            
        '''
        if type(other) == type(self):
            return id(self) == id(other)
        return self.value == other
        
    def __iter__( self ):
        '''
        for child in item
        
        Iterate on children.
        '''
        # we must use SelectItems to have order and groups
        # TODO: the _selecter methods are generators, check if this breaks it:
        return iter( self._selecter.SelectItems(self,'*') )

    def __lshift__(self, expression ):
        '''
        item << expression << expression
        
        Change the sort order used by methods returning items.
        Calling this has no cost, job is done each time you access
        items thru this one.
        
        'expression' will be evaluated by every child to resolve the order.
        Use None to reset the sort order to default (undefined but way faster)

        Returns self so you can do:
            item << 'name' << 'date'

        If speed mayyers, you should consider using this form:
            item << '(@CHILD.name,@CHILD.date)'
        instead of this:
            item << '@CHILD.name' << '@CHILD.date'
        since every expression must be evaluated on every items.
        One expression is faster than several, unless every expression is really
        simple. 
        
        NB: See python documentation for operator precedence order.
        
        '''
        if expression is None:
            self._sorter.Clear()
        else:
            self._sorter.AddBy( expression )
        return self
    
    def __getitem__( self, select ):
        '''
        item[3]
        item[-1]
        item[2:4]
        item[2:-1]
        item[2:-1:2]
        item[:]
        item[path]
        item[wildcard]
        item[wildcard:where]
        item[:where]
        
        Retreive an Item or a Group ordered as previously requested.
        (see __lshift__ for ordering options)
        
        ** 'path' is the id of the item to retreive.
        If it does not start with '/', it is relative to self.id 
            item['TASK/Compo']
            item['/ENUMS/STATUS/WIP']
        The return value is a single Item.
        
        ** 'wildcard' works like path but retreives a Group:
            item['ENUM/*']
            item['ENUM/STATUS/*']
        The return value is a Group.
            
        If 'where' is given but not 'path' nor 'wildcard', the
        default value is used: '*'
                
        ** 'where' is an expression (see __call__):
        It will be evaluated by each item found by 'path' or 'wildcard'.
        If the evaluation equals True, the item will be part of the returned
        value (Item or Group).
        
        If 'path' or 'wilcard' is given but not 'where', the default values
        is used: 'None', which always evaluates to True.
        
        '''

        wildcard, where = None, None
        
        # item[index]
        if isinstance( select, int ):
            return self._selecter.GetIndex(self,select)

        # item[path]
        # item[wilcard]
        elif isinstance( select, basestring ):
            return self._selecter.Select( self, select )

        # item[x:y]
        # item[x:y:z]    
        elif isinstance( select, slice ):
            if (
                    # item[start:]
                    # item[start:stop]
                    # item[:stop]
                    # item[:stop:]
                    # item[:stop:step]
                    # item[start:stop:step]
                    isinstance(select.stop,int):
                or
                    # item[::step]
                    select.stop is None and isinstance(select.step,int)
                ):
                return self._selecter.GetRange( self, select )
        
            # item[path:where]
            # item[wildcard:where]
            elif (
                    isinstance( select.start, basestring ) 
                and
                    isinstance(select.stop, basestring)
                ):
                return self._selecter.Select( self, select.start, select.end )

        raise TypeError("Bad select: "+repr(select))
        
        
#
#--- Item
#
class Item( _AbstractItem ):
    '''
    A DataK Item.
    '''


#
#--- Link
#
class Link( _AbstractItem ):
    '''
    A DataK Link Item.
    
    It's like an Item, but attribute strategy is delegated to
    the target item.
    
    '''
    @classmethod
    def _SingleKey( klass, cnt, id, target ):
        '''
        Singletonize instances on ( Item, cnt, id)
        so you can't create an Item AND a Link with the same id
        '''
        # that's kind of tricky: we use Item instead of klass (which is Link)
        # we also dont use target so that only one Link with this id can exists.
        return Item, cnt, id 

    def __init__( self, cnt, id, targetId ):
        _AbstractItem.__init__( self, cnt, id )
        self._targetId = targetId
        self._targetItem = cnt.GetItem( targetId )

    def __getattr__( self, name ):
        '''
        See _AbstractItem.__getattr__
        
        Adds too special attributes:
            targetId
            targetItem
        '''
        # property like attributes:
        if name == 'targetId':
            return self._targetId
        elif name == 'targetItem':
            return self._targetItem

        # other attributes:
        _AbstractItem.__getattr__( self, name )

    def __setattr__( self, name, value ):
        '''
        See _AbstractItem.__setattr__
        
        Trying to set on those attributes is an error:
            targetId
            targetItem
        
        '''
        # read only property like attributes
        if name in ('targetId','targetItem'):
            raise ValueError( name+' is Read Only!') 

        # other attributes:
        _AbstractItem.__setattr__( self, name, value )


#
#--- Group
#
class Group( _AbstractItem ):
    '''
    A Group is a collection of Item, Link or Group.
    
    It's an Item with special behavior:
    * it has no id, path, name and doesn't exits on disk.
    * it is not a singleton, each Group instances are different.
    * iterating occurs on the contained items.
    * Setting an attribute on a Group sets it on all the contained items.
    * Getting an attribute will retrun None until all contained items
    have the same value for this attribute (which might still be None).
    
    You can use += and -= to append and remove item(s):
        group += item           # add item in group
        group += group2         # add group2 in group
        group += (item1,item2)  # add item1 and item2 in group
        group += group2[:-1]    # add all items but the last one in group
        group += group2[:]      # add all items in group
        
        group -= item           # remove item from group (may raise)
        group -= group2         # remove group2 from group (may raise)
        group -= (item1,item2)  # remove item1 and item2 from group (may raise)
        group -= group2[:]      # remove item in group2 from group (may raise)
        
    '''
    @classmethod
    def _SingleKey( klass, *args ):
        '''
        Returns None to not singletonize Groups.
        '''
        return None
        
    def __init__( self, cnt, *items ):
        _AbstractItem.__init__( self, cnt, '' )
        self._items = list(items)
    
    def __nonzero__( self ):
        '''
        If group:
            print 'I has items'
        else:
            print 'No items'
        '''
        if self._items:
            return True
        return False
    
    def __eq__( self, other):
        '''
        groupX == groupY
        group == 18
        
        Two groups are equal if they contain the same items.
        An group is equal to something else if every contained
        Item is.
        
        To test if two groups have the same value, you can:
            groupX == groupY.value
        but this will never be True:
            groupX.value == groupY
        
        '''
        if type(other) == type(self):
            return other._items == self._items
        return self.value == other
        
    def __iter__( self ):
        return iter( self._items )

    def __lshift__(self, expression ):
        '''
        group << expression << expression
        
        Sort the contained items.
        See Item documentation for more syntaxe details.
        
        Unlike Item, Group are ordered at request, so you should
        avoid sorting many times.
        
        '''
        if expression is None:
            self._sorter.Clear()
        else:
            self._sorter.AddBy( expression )
        self._items = self._sorter.Do( self, self._items, False )
        return self
    
    def __div__( self, expression ):
        '''
        grouped = group / expression
        
        Return a dict with the result of expression as key and a Group
        containing all items from this Group giving the key as expression
        result.
        
        '''
        # TODO: use itertools.groupby
        return self._grouper.GroupBy( expression )

    def __floordiv__( self, expression ):
        '''
        separated = group // expression
        
        Return a tuple of 2D items like
            (
                (expResult, Group),
                (expResult, Group),
                (expResult, Group),
                ...
            )
        '''
        # TODO: use itertools.groupby
        return self._grouper.SeparateBy( expression )
        
    def __iadd__( self, other ):
        '''
        group += item           => append item.
        group += group          => append group.
        group += (itemX,itemY)  => extend with (itemX,itemY).
        group += item[:]        => extend with all item children.
        group += group[:]       => extend with all group items.
        
        Returns self so you can do:
            group += itemX += itemY
            
        '''
        if isinstance( other, _AbstractItem ):
            # items/links/groups are added directly, not their content
            self._items.append( other )
        elif hasattr( other, '__iter__' ):
            for i in other:
                if not insinstance( i, _AbstractItem ):
                    raise TypeError( "Cannot append to Group: "+repr(i) )
                self._items.append( i )
        else:
            raise TypeError( "Cannot append to Group: "+repr(other) )
        return self
    
    def __isub__( self, other ):
        '''
        group -= item           => remove item.
        group -= group          => remove group.
        group -= (itemX,itemY)  => remove itemX and itemY.
        group -= item[:]        => remove all children of item.
        group -= group[:]       => remove all items contained by group.
        
        Returns self so you can do:
            group -= itemX -= itemY
        
        Will not raise an error if the item(s) to remove are not present.
        '''
        if isinstance( other, _AbstractItem ):
            # items/links/groups are removed directly, not their content
            try:
                self._items.remove( other )
            except ValueError:
                pass
        elif hasattr( other, '__iter__' ):
            [ self._items.remove(o) for o in set(other).intersection(set(self._items)) ]
        else:
            raise TypeError( "Cannot remove from Group: "+repr(other) )
        return self


